JPA와 REST API로 댓글 기능 만들기 2
✒️ 2025-05-28 13:10 내용 수정
스프링부트3 자바 백엔드 개발입문 내용 참고 및 정리
- 게시판 만들기에서는 MyBatis를 사용하여 게시판 기능을 만들었다.
- 이번에는 JPA(Java Persistence API)와 Spring boot로 REST API 구현(JPA)를 통해 간단하게 만든 게시판에 댓글 기능을 추가하였다.
- JPA와 REST API로 댓글 기능 만들기에서 만든 REST API를 사용하여 이번엔 페이지에서 댓글을 조회, 추가, 수정, 삭제하는 동작을 추가한다.
- View를 생성한다.
1. 댓글 조회
- 먼저 댓글을 조회하기 위해선 JPA로 DB Read 수행하기에서 Controller가
src/main/resources/templates폴더 내의mustache파일을 반환했다는 것을 떠올려보자.- 이를 위해
src/main/resources/templates폴더에comments라는 폴더를 생성하고, 폴더 내에_comments.mustache파일을 생성한다. - Mustache 참고.
- 이를 위해
_comments.mustache는 게시글 페이지에서 댓글 목록 및 생성을 보여줄 항목이므로, 게시글 상세보기 페이지인show.mustache파일에서_comments.mustache파일을 출력하도록 설정한다.show.mustache파일은 JPA로 DB Read 수행하기에서 작성한 파일이다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- bootstrap code 생략 -->
<title>SpringBoot Example</title>
</head>
<body>
{{> layouts/header }}
<!-- 중략 -->
{{! 댓글 파일 }}
{{> comments/_comments }}
{{> layouts/footer }}
<script>
function check() { // 삭제 확인용
if (confirm("삭제하시겠습니까?") != false) {
location.href='/articles/{{article.id}}/delete';
}
}
</script>
</body>
</html>
_comments.mustache는 댓글 기능을 담당하는 View 역할로, 댓글 목록과 댓글 추가 기능을 위한 View 파일인_list.mustache파일과_new.mustache파일을src/main/resources/templates/comments폴더 내에 추가한다._comments.mustache에_list.mustache파일과_new.mustache파일을 가져오는 코드를 추가한다.- 댓글 조회에선
_list.mustache파일만 먼저 작성한다.
{{! _comments.mustache }}
<div>
{{> comments/_list }}
{{> comments/_new }}
</div>
_list.mustache에서 댓글을 보여주기 위한 영역을 설정한다.- Bootstrap의 card 요소를 사용하였다.
- Bootstrap Cards
<div id="comments-list">
{{! card 영역에선 DTO의 field를 사용하도록 설정 }}
{{#commentDtos}}
<div class="card m-2" id="comments-{{id}}">
<div class="card-header">
{{nickname}}
</div>
<div class="card-body">
{{body}}
</div>
</div>
{{/commentDtos}}
</div>
- 이제 View에 데이터를 전달해줄 Model을 생성하기 위해
ArticleController클래스의 메소드를 수정한다.- 특정 게시글을 상세보기 할 때 해당 게시글에 작성된 댓글을 가져와야 하므로
commentService.comment()를 통해 DB에서 댓글 데이터를 가져온다. - JPA와 REST API로 댓글 기능 만들기#2-1. 댓글 REST API - GET 요청에서 특정 게시글의 모든 댓글을 조회하는
CommentService메소드를 추가했다. - 기존
ArticleController에는CommentService가 없었기에 의존성 주입(자동 주입)을 설정한다.
- 특정 게시글을 상세보기 할 때 해당 게시글에 작성된 댓글을 가져와야 하므로
package com.example.demo.controller;
import com.example.demo.DTO.ArticleForm;
import com.example.demo.DTO.CommentDto;
import com.example.demo.entity.Article;
import com.example.demo.entity.Comment;
import com.example.demo.repository.ArticleRepository;
import com.example.demo.service.CommentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
@Controller
@RequestMapping("/articles")
@Slf4j // simple logging facade for java
public class ArticleController {
@Autowired
private ArticleRepository articleRepository;
@Autowired
private CommentService commentService;
// ...생략
@GetMapping("{id}") // 경로 매개변수 사용
public String show(@PathVariable Long id, Model model) {
log.info("id = " + id); // 로그 확인
Article articleEntity = articleRepository.findById(id).orElse(null); // id로 조회
List<CommentDto> commentDtos = commentService.comments(id); // 댓글 가져오기
model.addAttribute("article", articleEntity);
model.addAttribute("commentDtos", commentDtos);
return "/articles/show"; // id로 조회할 데이터를 보여줄 view
}
// ...생략
}
- 이제 브라우저에
http://localhost:port/로 접속한 후 댓글이 작성된 더미 데이터의 게시글 상세보기 페이지로 이동해 댓글을 가져왔는지 확인한다.- Test와 Test 코드 1에서 더미 데이터 설정을 진행했다.
2. 댓글 추가
- 앞서 위에서 생성해둔
_new.mustache파일에 댓글 추가를 위한 내용을 구성한다.- Bootstrap의 card 요소의 body와 form 요소를 사용하였다.
- Bootstrap Body, Bootstrap forms
- JPA와 REST API로 댓글 기능 만들기에서 만든
CommentApiController의 REST API들을 호출하여 사용하기 위해 Javascript를 사용하여fetch()로 요청을 보낸다.- 선택자(Selector), Fetch 참고.
{{! _new.mustache }}
<div class="card m-2" id="comment-new">
<div class="card-body">
<form>
<!-- 닉네임 입력 -->
<div class="mb-3">
<label for="new-comment-nickname" class="form-label">닉네임</label>
<input type="text" class="form-control" id="new-comment-nickname">
</div>
<!-- 댓글 본문 -->
<div class="mb-3">
<label for="new-comment-body" class="form-label">댓글 내용</label>
<textarea class="form-control" rows="3" id="new-comment-body"></textarea>
</div>
{{! 댓글의 articleId를 넘겨주기 위한 hidden input }}
{{! ArticleController의 show에서 model은 article과 commentDtos를 View에 보내줌 }}
{{#article}}
<input type="hidden" id="new-comment-article-id" value="{{id}}">
{{/article}}
<button type="button" class="btn btn-primary" id="comment-create-btn">작성</button>
</form>
</div>
</div>
<script>
{
// button 가져오기
const commentCreateBtn = document.querySelector("#comment-create-btn");
// click event listener 추가
commentCreateBtn.addEventListener("click", function() {
// input 내용으로 comment 객체 생성
const comment = {
nickname: document.querySelector("#new-comment-nickname").value,
articleId: document.querySelector("#new-comment-article-id").value,
body: document.querySelector("#new-comment-body").value,
}
// fetch로 REST API 요청
let url = `/api/articles/${comment.articleId}/comments`; // url 설정
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(comment) // 객체 -> JSON 변환
}).then(res => {
const msg = (res.ok) ? "댓글이 등록됐습니다." : "댓글 등록 실패";
alert(msg);
if (res.ok) {
window.location.reload(); // 새로고침
}
});
});
}
</script>
- 여기서 잠시 테스트를 위해 새 댓글을 추가했는데 게시글 id가 다르다는 예외가 발생했다.
- 내용을 살펴보니 JSON의 key와
CommentDto의 Field가 일치하지 않아 생긴 문제였다. - 문제를 해결하고 댓글 생성란에 닉네임과 내용을 입력 후 작성 버튼을 누르면 댓글이 잘 추가된 것을 확인할 수 있다.
3. 댓글 수정
- 이번엔 댓글을 수정하기 위해
_list.mustache파일에 bootstrap 모달(Modal)과_new.mustache에서 사용한<form>을 가져와 수정 데이터를 받을 영역을 추가한다.- bootstrap modal : Bootstrap modal
- Modal을 여는 버튼은 댓글마다 존재하지만, Modal은 1개만 사용하기 때문에 Modal에 id와 articleId를 넣어줄 때
button에 지정한data-속성을 사용하여 정보를 저장한다. - Javascript의
querySelector()로 값을<input type="hidden">인 input에 넣어서fetch()로 보낸다.
<!-- _list.mustache -->
<div id="comments-list">
{{! card 영역에선 DTO의 field를 사용하도록 설정 }}
{{#commentDtos}}
<div class="card m-2" id="comments-{{id}}">
<div class="card-header">
{{nickname}}
<!-- Modal 여는 버튼 -->
<button type="button"
class="btn btn-sm btn-outline-primary mx-2"
data-bs-toggle="modal"
data-bs-target="#comment-edit-modal"
data-bs-id="{{id}}"
data-bs-article-id="{{articleId}}"
data-bs-nickname="{{nickname}}"
data-bs-body="{{body}}"
>
수정
</button>
</div>
<div class="card-body">
{{body}}
</div>
</div>
{{/commentDtos}}
</div>
<!-- Modal -->
<div class="modal fade" id="comment-edit-modal" tabindex="-1"
aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="updateModalLabel">댓글 수정</h1>
<button type="button" class="btn-close"
data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- 댓글 수정 form -->
<form>
<!-- 닉네임 입력 -->
<div class="mb-3">
<label for="edit-comment-nickname"
class="form-label">닉네임</label>
<input type="text"
class="form-control" id="edit-comment-nickname">
</div>
<!-- 댓글 본문 -->
<div class="mb-3">
<label for="edit-comment-body"
class="form-label">댓글 내용</label>
<textarea class="form-control"
rows="3" id="edit-comment-body"></textarea>
</div>
<!-- 댓글 id와 articleId 전달 -->
<input type="hidden"
id="edit-comment-id">
<input type="hidden"
id="edit-comment-article-id">
<button type="button" class="btn btn-primary"
id="comment-update-btn">수정완료</button>
</form>
</div>
</div>
</div>
</div>
- 이제 Javascript를 사용하여 Modal의 이벤트 처리를 추가하고,
fetch()로 REST API를 호출하여 수정 데이터를 보낼 수 있도록<script>를 작성한다.- Modal의 Event : Boostrap event
| Event | 설명 |
|---|---|
hide.bs.modal |
Modal이 숨겨지기 직전에 실행 |
hidden.bs.modal |
Modal이 숨겨진 후 실행 |
show.bs.modal |
Modal이 보여지기 직전 실행 |
shown.bs.modal |
Modal이 보여진 후 실행 |
<!-- _list.mustache -->
<div>
<!-- 위에서 작성해서 생략 -->
</div>
<!-- modal evnet -->
<script>
{
// modal 선택
const commentEditModal = document.querySelector("#comment-edit-modal");
commentEditModal.addEventListener("show.bs.modal", function(event) {
// event = show.bs.modal
// event.target = modal
// 트리거 버튼 선택
const triggerBtn = event.relatedTarget; // modal 여는 버튼
// 데이터 가져오기
const id = triggerBtn.getAttribute("data-bs-id");
const articleId = triggerBtn.getAttribute("data-bs-article-id");
const nickname = triggerBtn.getAttribute("data-bs-nickname");
const body = triggerBtn.getAttribute("data-bs-body");
// 데이터를 modal form에 반영
document.querySelector("#edit-comment-id").value = id;
document.querySelector("#edit-comment-article-id").value = articleId;
document.querySelector("#edit-comment-nickname").value = nickname;
document.querySelector("#edit-comment-body").value = body;
});
}
{
// 수정 요청 보내기
const commentUpdateBtn = document.querySelector("#comment-update-btn");
commentUpdateBtn.addEventListener("click", function() {
// 댓글 객체 생성
const comment = {
id: document.querySelector("#edit-comment-id").value,
articleId: document.querySelector("#edit-comment-article-id").value,
nickname: document.querySelector("#edit-comment-nickname").value,
body: document.querySelector("#edit-comment-body").value
}
// 수정 url 생성
const url = `/api/comments/${comment.id}`;
// fetch
fetch(url, {
method: "PATCH", // 수정을 위한 PATCH 요청
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(comment) // 객체 -> JSON 변환
}).then(res => {
const msg = (res.ok) ? "댓글이 수정됐습니다." : "댓글 수정 실패";
alert(msg);
if (res.ok) {
window.location.reload(); // 새로고침
}
});
});
}
</script>
- 코드 작성을 완료하고
PackageNameApplication을 Run한 후, 브라우저에 댓글이 작성된 게시글 상세보기 페이지로 이동한다.
- 댓글 수정 버튼을 누르면 댓글 정보가 form에 출력되는 것을 확인할 수 있고, 내용을 수정해서 수정이 정상적으로 완료되면 댓글의 바뀐 내용도 확인할 수 있다.
4. 댓글 삭제
- 댓글 수정에서 만든 Modal을 여는 버튼 코드 바로 아래에 댓글 삭제를 위한
button을 추가한다.
<!-- _list.mustache -->
<div id="comments-list">
{{! card 영역에선 DTO의 field를 사용하도록 설정 }}
{{#commentDtos}}
<div class="card m-2" id="comments-{{id}}">
<div class="card-header">
{{nickname}}
<!-- Modal 여는 버튼 -->
<button type="button"
class="btn btn-sm btn-outline-primary mx-2"
data-bs-toggle="modal"
data-bs-target="#comment-edit-modal"
data-bs-id="{{id}}"
data-bs-article-id="{{articleId}}"
data-bs-nickname="{{nickname}}"
data-bs-body="{{body}}"
>
수정
</button>
<button type="button"
class="btn btn-sm btn-outline-danger comment-delete-btn"
data-bs-id="{{id}}">
삭제
</button>
</div>
<div class="card-body">
{{body}}
</div>
</div>
{{/commentDtos}}
</div>
<!-- modal 부분 생략 -->
- Modal 이벤트 처리를 구현한
<script>코드 아래에 새<script>코드를 추가하여 삭제를 위한 REST API 호출을fetch()로 작성한다.- 교재에선 script 역할 구분 상으로 따로 작성한 것으로 보인다.
<!-- _list.mustache -->
<div>
<!-- 생략 -->
</div>
<!-- modal evnet -->
<script>
// ...생략
</script>
<!-- delete event -->
<script>
// 삭제 버튼 전체 선택
const commentDeleteBtns = document.querySelectorAll(".comment-delete-btn");
// 모든 삭제 버튼에 이벤트 리스너 추가
commentDeleteBtns.forEach(btn => {
btn.addEventListener("click", function(event) {
// 이벤트 발생 요소 가져오기
const commentDeleteBtn = event.target;
// 삭제 대상 id 가져오기
const id = commentDeleteBtn.getAttribute("data-bs-id");
// 삭제 재확인
if (!confirm(`${id}번 댓글을 삭제하시겠습니까?`)) {
return; // 취소 시 삭제 동작 중단
}
// url 생성
const url = `/api/comments/${id}`;
fetch(url, {
method: "DELETE" // 삭제 요청을 위한 DELETE
}).then(res => {
if (!res.ok) { // 삭제 실패 시 처리
alert("댓글 삭제 실패");
return;
}
// 댓글 삭제 알림
const target = document.querySelector(`#comments-${id}`);
target.remove(); // 댓글을 View에서도 삭제
alert(`${id}번 댓글을 삭제했습니다`);
window.location.reload(); // 새로고침
});
});
});
</script>
- 브라우저에서 댓글이 작성된 페이지로 이동하면 댓글의 수정버튼 옆에 삭제 버튼을 확인할 수 있다.
- 삭제 버튼을 누르면 삭제를 재확인하는 창이 뜨고, 확인을 누르면 삭제 완료와 함께 해당 댓글이 더 이상 보이지 않는다.